/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.collector;

import com.google.apphosting.api.DeadlineExceededException;
import flexjson.JSONException;
import flexjson.JSONSerializer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import za.org.foobar.famon.Serializer;
import za.org.foobar.famon.dto.*;
import za.org.foobar.famon.model.*;
import za.org.foobar.famon.model.StackTraceElement;
import za.org.foobar.famon.util.DB;
import za.org.foobar.famon.webapp.ApplicationLogic;

import javax.servlet.ServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/** 
 *
 */
public class FamonCollectorLogic {
    private final Log log = LogFactory.getLog(getClass());
    SourceLocationStore sourceLocationStore = SourceLocationStore.get();

    private static FamonCollectorLogic instance;
    public static FamonCollectorLogic get() {
        return instance == null ? instance = new FamonCollectorLogic() : instance;
    }
    
    public FamonCollectorLogic() {
        log.debug("Creating FamonCollectorLogic");
    }

    public void handleRequest(ServletRequest request, String json) throws JSONException {

//        boolean queueFailures = Property.getBooleanProperty("queueFailures", true);
//        if (queueFailures) {
            FailureQueue queue = new FailureQueue(json);
            queue.setSourceHostname(request.getRemoteHost());
            queue.setSourceAddress(request.getRemoteAddr());
            queue.setSourcePort(request.getRemotePort());
            DB.persist(queue);
//        }  else {
//            CFailureBatch batch = Serializer.deserializeFailureBatch(json);
//            if (log.isDebugEnabled()) {
//                log.debug("Parsed failure batch: \n" + new JSONSerializer().exclude("*.class").deepSerialize(batch));
//            }
//            processFailureBatch(batch, request.getRemoteHost(), request.getRemoteAddr(), request.getRemotePort());
//        }
        DB.closePM();
    }


    public Application getApplicationByNameTx(String name) {
        DB.begin();
        try {
            return getApplicationByName(name);
        } finally {
            DB.commit();
        }
    }

    public Application getApplicationByName(String name) {
        log.info(Thread.currentThread().getName() + ": Getting application: " + name);
        Application application = DB.findByName(Application.class, name);
        if (application != null) {
            log.info(Thread.currentThread().getName() + ": Found: " + application);
            return application;
        }
        application = new Application(name);
        DB.persist(application);
        log.info(Thread.currentThread().getName() + ": New: " + application);
        return application;
    }

    public synchronized void processQueue(OutputStream out) {
        final int totalProcessingTime = 28000;
//        StreamHandler handler = new StreamHandler(out, new SimpleFormatter());
//        handler.setLevel(Level.FINE);
//        log.addHandler(handler);
        long startTime = System.currentTimeMillis();
        long t = startTime;
        FailureQueue currentFailure = null;
        int maxFailuresToProcess = Property.getIntegerProperty("maxFailuresToProcess", 5);
        try {
            List<FailureQueue> list = DB.createDBQuery(FailureQueue.class)
                    .addWhere("failed == false")
                    .addOrderBy("dateReceived desc")
                    .setRange(0, maxFailuresToProcess)
                    .getResultList();
            t = logTime(t, "Queried queue of failures", 1);
            if (list == null || list.size() == 0) {
                log.info("No failures in queue");
                return;
            }
            int timeToProcessFailure = Property.getIntegerProperty("timeToProcessFailure", 8000);
            log.info("Processing " + list.size() + " requests");

            for (FailureQueue f : list) {
                currentFailure = f;
                CFailureBatch batch = Serializer.deserializeFailureBatch(f.getJson().getValue());
                log.debug("Processing failure from " + batch.getApplication() + ", version " + batch.getVersion() +
                        ", instance " + batch.getInstance());
                if (log.isTraceEnabled()) {
                    log.trace("Parsed failure batch: \n" + new JSONSerializer().exclude("*.class").deepSerialize(batch));
                }
                processFailureBatch(batch, f.getSourceHostname(), f.getSourceAddress(), f.getSourcePort());
                DB.remove(f);
                currentFailure = null;
                t = logTime(t, "Finished failure", 1);
                log.debug("Time left: " + (totalProcessingTime - (t - startTime)));
                if (totalProcessingTime - (t - startTime) < timeToProcessFailure) {
                    log.info("Not enough time for another failure");
                    return;
                }
            }
        } catch (DeadlineExceededException e) {
            log.info("Deadline Exceeded, total time: " + (System.currentTimeMillis() - startTime));
        } catch (Exception e) {
            log.error("Failed processing queued failure", e);
            if (currentFailure != null) {
                currentFailure.setFailed(true);
                DB.persist(currentFailure);
            }
        } finally {
            DB.closePM();
//            log.removeHandler(handler);
//            handler.flush();
            try {
                out.close();
            } catch (IOException ignored) {
            }
            logTime(t, "Done Process Queue", 1);
            log.info("Done");
            LogFactory.releaseAll();
        }
    }

    private long logTime(long t, String message, int depth) {
        long v;
        v = System.currentTimeMillis();
        log.debug(Thread.currentThread().getName() + ": " + "             ".substring(0,depth)
                + message + ": " + (v-t) + "ms");
        t = v;
        return t;
    }

    private void processFailureBatch(CFailureBatch batch, String sourceHostName, String sourceAddress, int sourcePort) {
        long t = System.currentTimeMillis();
        Application application = getApplicationByNameTx(batch.getApplication());
        t = logTime(t, "getApplicationByName", 1);
        ApplicationVersion version = application.getVersionByNameTx(batch.getVersion());
        t = logTime(t, "getVersionByName", 1);
        ApplicationInstance instance = application.getInstanceByNameTx(batch.getInstance());
        t = logTime(t, "getInstanceByName", 1);
        ApplicationGroup group = ApplicationLogic.get().getApplicationGroupTx(application, instance, version);
        t = logTime(t, "getApplicationGroup", 1);
        for (CFailure failure : batch.getFailures()) {
            log.info("Creating Failure for: App: " + application.getId() + ", Instance: " + instance.getId() +
                    ", Version: " + version.getId());
            processFailure(group, version, failure,
                    sourceHostName, sourceAddress, sourcePort,
                    batch.getSourceAddress(), batch.getSourceHostname());
            t = logTime(t, "processFailure", 2);
        }
    }

    private void processFailure(ApplicationGroup group, ApplicationVersion version, CFailure failureDto,
                                String sourceHostName, String sourceAddress, int sourcePort,
                                String originalSourceAddress, String originalSourceHostname) {
        long t = System.currentTimeMillis();
        DB.begin();
        List<CThreadInformation> threadDtoList = failureDto.getThreads();
        for (CThreadInformation threadDto : threadDtoList) {
            addSourceLocations(version, threadDto);
        }
        sourceLocationStore.createEntries();
        DB.commit();
        t = logTime(t, "Created SourceLocationStore entries", 5);
        
        DB.begin();
        Failure failure = new Failure(group.getId(), failureDto.getUser(), failureDto.getFailureType(), 
                failureDto.getMessage(), failureDto.getDate());
        failure.setSourceHostname(sourceHostName);
        failure.setSourceAddress(sourceAddress);
        failure.setSourcePort(sourcePort);
        failure.setOriginalSourceAddress(originalSourceAddress);
        failure.setOriginalSourceHostname(originalSourceHostname);

        t = logTime(t, "new Failure", 3);

        List<ThreadInformation> threadList = new ArrayList<ThreadInformation>(threadDtoList.size());
        for (CThreadInformation threadDto : threadDtoList) {
            ThreadInformation thread = createThreadInformation(version, threadDto);
            threadList.add(thread);
            t = logTime(t, "processThread", 4);
        }
        failure.setThreadList(threadList);
        t = logTime(t, "set Thread List", 3);
        DB.persist(failure);
        logTime(t, "new Failure persisted", 3);
        DB.commit();
        logTime(t, "Commit", 5);
    }

    private void addSourceLocations(ApplicationVersion applicationVersion, CThreadInformation threadDto) {
        long t = System.currentTimeMillis();

        List<CStackTraceElement> stackTraceDto = threadDto.getStackTrace();
        for (CStackTraceElement stackTraceElementDto : stackTraceDto) {
            sourceLocationStore.addSourceLocation(applicationVersion, stackTraceElementDto);
        }
        t = logTime(t, "addSourceLocation - stack trace of size " + stackTraceDto.size(), 5);

        List<CLoggingEvent> loggingEventDtoList = threadDto.getLoggingEvents();
        for (CLoggingEvent loggingDto : loggingEventDtoList) {
            sourceLocationStore.addSourceLocation(applicationVersion, loggingDto);
        }
        logTime(t, "addSourceLocation - logging events of size " + loggingEventDtoList.size(), 5);
    }

    private ThreadInformation createThreadInformation(ApplicationVersion applicationVersion, CThreadInformation threadDto) {
        long t = System.currentTimeMillis();

        int index = 0;
        ThreadInformation thread = new ThreadInformation(threadDto.getName());
        t = logTime(t, "New Thread", 5);
        List<CStackTraceElement> stackTraceDto = threadDto.getStackTrace();
        List<StackTraceElement> stackTrace = new ArrayList<StackTraceElement>(stackTraceDto.size());
        for (CStackTraceElement stackTraceElementDto : stackTraceDto) {
            SourceLocation sourceLocation = sourceLocationStore.getSourceLocation(applicationVersion, stackTraceElementDto);
            StackTraceElement element = new StackTraceElement(index, sourceLocation);
            stackTrace.add(element);
            index++;
        }
        thread.setStackTrace(stackTrace);
        t = logTime(t, "addStackTraceElement of size " + stackTraceDto.size(), 5);

        index = 0;
        List<CLoggingEvent> loggingEventDtoList = threadDto.getLoggingEvents();
        List<LoggingEvent> loggingEventList = new ArrayList<LoggingEvent>(loggingEventDtoList.size());
        for (CLoggingEvent loggingDto : loggingEventDtoList) {
            SourceLocation sourceLocation = sourceLocationStore.getSourceLocation(applicationVersion, loggingDto);
            LoggingEvent log = new LoggingEvent(index, loggingDto.getLoggerName(),
                    loggingDto.getLevel(), loggingDto.getMessage(), sourceLocation);
            loggingEventList.add(log);
            index++;
        }
        thread.setLoggingEventList(loggingEventList);
        logTime(t, "addLoggingEvent of size " + loggingEventDtoList.size(), 5);
        return thread;
    }
}
